home *** CD-ROM | disk | FTP | other *** search
/ PC go! 2008 October / PCgo 2008-10 (DVD).iso / interface / contents / vollversionen_6617 / 21733 / files / xulrunner / components / nsLoginManager.js < prev    next >
Encoding:
Text File  |  2008-08-20  |  43.0 KB  |  1,236 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is mozilla.org code.
  15.  *
  16.  * The Initial Developer of the Original Code is Mozilla Corporation.
  17.  * Portions created by the Initial Developer are Copyright (C) 2007
  18.  * the Initial Developer. All Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *  Justin Dolske <dolske@mozilla.com> (original author)
  22.  *
  23.  * Alternatively, the contents of this file may be used under the terms of
  24.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  25.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  26.  * in which case the provisions of the GPL or the LGPL are applicable instead
  27.  * of those above. If you wish to allow use of your version of this file only
  28.  * under the terms of either the GPL or the LGPL, and not to allow others to
  29.  * use your version of this file under the terms of the MPL, indicate your
  30.  * decision by deleting the provisions above and replace them with the notice
  31.  * and other provisions required by the GPL or the LGPL. If you do not delete
  32.  * the provisions above, a recipient may use your version of this file under
  33.  * the terms of any one of the MPL, the GPL or the LGPL.
  34.  *
  35.  * ***** END LICENSE BLOCK ***** */
  36.  
  37.  
  38. const Cc = Components.classes;
  39. const Ci = Components.interfaces;
  40.  
  41. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  42.  
  43. function LoginManager() {
  44.     this.init();
  45. }
  46.  
  47. LoginManager.prototype = {
  48.  
  49.     classDescription: "LoginManager",
  50.     contractID: "@mozilla.org/login-manager;1",
  51.     classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
  52.     QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager,
  53.                                             Ci.nsISupportsWeakReference]),
  54.  
  55.  
  56.     /* ---------- private memebers ---------- */
  57.  
  58.  
  59.     __logService : null, // Console logging service, used for debugging.
  60.     get _logService() {
  61.         if (!this.__logService)
  62.             this.__logService = Cc["@mozilla.org/consoleservice;1"].
  63.                                 getService(Ci.nsIConsoleService);
  64.         return this.__logService;
  65.     },
  66.  
  67.  
  68.     __ioService: null, // IO service for string -> nsIURI conversion
  69.     get _ioService() {
  70.         if (!this.__ioService)
  71.             this.__ioService = Cc["@mozilla.org/network/io-service;1"].
  72.                                getService(Ci.nsIIOService);
  73.         return this.__ioService;
  74.     },
  75.  
  76.  
  77.     __formFillService : null, // FormFillController, for username autocompleting
  78.     get _formFillService() {
  79.         if (!this.__formFillService)
  80.             this.__formFillService =
  81.                             Cc["@mozilla.org/satchel/form-fill-controller;1"].
  82.                             getService(Ci.nsIFormFillController);
  83.         return this.__formFillService;
  84.     },
  85.  
  86.  
  87.     __storage : null, // Storage component which contains the saved logins
  88.     get _storage() {
  89.         if (!this.__storage) {
  90.  
  91.             var contractID = "@mozilla.org/login-manager/storage/legacy;1";
  92.             try {
  93.                 var catMan = Cc["@mozilla.org/categorymanager;1"].
  94.                              getService(Ci.nsICategoryManager);
  95.                 contractID = catMan.getCategoryEntry("login-manager-storage",
  96.                                                      "nsILoginManagerStorage");
  97.                 this.log("Found alternate nsILoginManagerStorage with " +
  98.                          "contract ID: " + contractID);
  99.             } catch (e) {
  100.                 this.log("No alternate nsILoginManagerStorage registered");
  101.             }
  102.  
  103.             this.__storage = Cc[contractID].
  104.                              createInstance(Ci.nsILoginManagerStorage);
  105.             try {
  106.                 this.__storage.init();
  107.             } catch (e) {
  108.                 this.log("Initialization of storage component failed: " + e);
  109.                 this.__storage = null;
  110.             }
  111.         }
  112.  
  113.         return this.__storage;
  114.     },
  115.  
  116.     _prefBranch  : null, // Preferences service
  117.     _nsLoginInfo : null, // Constructor for nsILoginInfo implementation
  118.  
  119.     _remember : true,  // mirrors signon.rememberSignons preference
  120.     _debug    : false, // mirrors signon.debug
  121.  
  122.  
  123.     /*
  124.      * init
  125.      *
  126.      * Initialize the Login Manager. Automatically called when service
  127.      * is created.
  128.      *
  129.      * Note: Service created in /browser/base/content/browser.js,
  130.      *       delayedStartup()
  131.      */
  132.     init : function () {
  133.  
  134.         // Cache references to current |this| in utility objects
  135.         this._webProgressListener._domEventListener = this._domEventListener;
  136.         this._webProgressListener._pwmgr = this;
  137.         this._domEventListener._pwmgr    = this;
  138.         this._observer._pwmgr            = this;
  139.  
  140.         // Preferences. Add observer so we get notified of changes.
  141.         this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
  142.                            getService(Ci.nsIPrefService).getBranch("signon.");
  143.         this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
  144.         this._prefBranch.addObserver("", this._observer, false);
  145.  
  146.         // Get current preference values.
  147.         this._debug = this._prefBranch.getBoolPref("debug");
  148.  
  149.         this._remember = this._prefBranch.getBoolPref("rememberSignons");
  150.  
  151.  
  152.         // Get constructor for nsILoginInfo
  153.         this._nsLoginInfo = new Components.Constructor(
  154.             "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
  155.  
  156.  
  157.         // Form submit observer checks forms for new logins and pw changes.
  158.         var observerService = Cc["@mozilla.org/observer-service;1"].
  159.                               getService(Ci.nsIObserverService);
  160.         observerService.addObserver(this._observer, "earlyformsubmit", false);
  161.         observerService.addObserver(this._observer, "xpcom-shutdown", false);
  162.  
  163.         // WebProgressListener for getting notification of new doc loads.
  164.         var progress = Cc["@mozilla.org/docloaderservice;1"].
  165.                        getService(Ci.nsIWebProgress);
  166.         progress.addProgressListener(this._webProgressListener,
  167.                                      Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
  168.  
  169.  
  170.     },
  171.  
  172.  
  173.     /*
  174.      * log
  175.      *
  176.      * Internal function for logging debug messages to the Error Console window
  177.      */
  178.     log : function (message) {
  179.         if (!this._debug)
  180.             return;
  181.         dump("Login Manager: " + message + "\n");
  182.         this._logService.logStringMessage("Login Manager: " + message);
  183.     },
  184.  
  185.  
  186.     /* ---------- Utility objects ---------- */
  187.  
  188.  
  189.     /*
  190.      * _observer object
  191.      *
  192.      * Internal utility object, implements the nsIObserver interface.
  193.      * Used to receive notification for: form submission, preference changes.
  194.      */
  195.     _observer : {
  196.         _pwmgr : null,
  197.  
  198.         QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, 
  199.                                                 Ci.nsIFormSubmitObserver,
  200.                                                 Ci.nsISupportsWeakReference]),
  201.  
  202.  
  203.         // nsFormSubmitObserver
  204.         notify : function (formElement, aWindow, actionURI) {
  205.             this._pwmgr.log("observer notified for form submission.");
  206.  
  207.             // We're invoked before the content's |onsubmit| handlers, so we
  208.             // can grab form data before it might be modified (see bug 257781).
  209.  
  210.             try {
  211.                 this._pwmgr._onFormSubmit(formElement);
  212.             } catch (e) {
  213.                 this._pwmgr.log("Caught error in onFormSubmit: " + e);
  214.             }
  215.  
  216.             return true; // Always return true, or form submit will be canceled.
  217.         },
  218.  
  219.         // nsObserver
  220.         observe : function (subject, topic, data) {
  221.  
  222.             if (topic == "nsPref:changed") {
  223.                 var prefName = data;
  224.                 this._pwmgr.log("got change to " + prefName + " preference");
  225.  
  226.                 if (prefName == "debug") {
  227.                     this._pwmgr._debug = 
  228.                         this._pwmgr._prefBranch.getBoolPref("debug");
  229.                 } else if (prefName == "rememberSignons") {
  230.                     this._pwmgr._remember =
  231.                         this._pwmgr._prefBranch.getBoolPref("rememberSignons");
  232.                 } else {
  233.                     this._pwmgr.log("Oops! Pref not handled, change ignored.");
  234.                 }
  235.             } else if (topic == "xpcom-shutdown") {
  236.                 for (let i in this._pwmgr) {
  237.                   try {
  238.                     this._pwmgr[i] = null;
  239.                   } catch(ex) {}
  240.                 }
  241.                 this._pwmgr = null;
  242.             } else {
  243.                 this._pwmgr.log("Oops! Unexpected notification: " + topic);
  244.             }
  245.         }
  246.     },
  247.  
  248.  
  249.     /*
  250.      * _webProgressListener object
  251.      *
  252.      * Internal utility object, implements nsIWebProgressListener interface.
  253.      * This is attached to the document loader service, so we get
  254.      * notifications about all page loads.
  255.      */
  256.     _webProgressListener : {
  257.         _pwmgr : null,
  258.         _domEventListener : null,
  259.  
  260.         QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
  261.                                                 Ci.nsISupportsWeakReference]),
  262.  
  263.  
  264.         onStateChange : function (aWebProgress, aRequest,
  265.                                   aStateFlags,  aStatus) {
  266.  
  267.             // STATE_START is too early, doc is still the old page.
  268.             if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING))
  269.                 return;
  270.  
  271.             if (!this._pwmgr._remember)
  272.                 return;
  273.  
  274.             var domWin = aWebProgress.DOMWindow;
  275.             var domDoc = domWin.document;
  276.  
  277.             // Only process things which might have HTML forms.
  278.             if (!(domDoc instanceof Ci.nsIDOMHTMLDocument))
  279.                 return;
  280.  
  281.             this._pwmgr.log("onStateChange accepted: req = " +
  282.                             (aRequest ?  aRequest.name : "(null)") +
  283.                             ", flags = 0x" + aStateFlags.toString(16));
  284.  
  285.             // Fastback doesn't fire DOMContentLoaded, so process forms now.
  286.             if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) {
  287.                 this._pwmgr.log("onStateChange: restoring document");
  288.                 return this._pwmgr._fillDocument(domDoc);
  289.             }
  290.  
  291.             // Add event listener to process page when DOM is complete.
  292.             domDoc.addEventListener("DOMContentLoaded",
  293.                                     this._domEventListener, false);
  294.             return;
  295.         },
  296.  
  297.         // stubs for the nsIWebProgressListener interfaces which we don't use.
  298.         onProgressChange : function() { throw "Unexpected onProgressChange"; },
  299.         onLocationChange : function() { throw "Unexpected onLocationChange"; },
  300.         onStatusChange   : function() { throw "Unexpected onStatusChange";   },
  301.         onSecurityChange : function() { throw "Unexpected onSecurityChange"; }
  302.     },
  303.  
  304.  
  305.     /*
  306.      * _domEventListener object
  307.      *
  308.      * Internal utility object, implements nsIDOMEventListener
  309.      * Used to catch certain DOM events needed to properly implement form fill.
  310.      */
  311.     _domEventListener : {
  312.         _pwmgr : null,
  313.  
  314.         QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
  315.                                                 Ci.nsISupportsWeakReference]),
  316.  
  317.  
  318.         handleEvent : function (event) {
  319.             this._pwmgr.log("domEventListener: got event " + event.type);
  320.  
  321.             var doc, inputElement;
  322.             switch (event.type) {
  323.                 case "DOMContentLoaded":
  324.                     doc = event.target;
  325.                     this._pwmgr._fillDocument(doc);
  326.                     return;
  327.  
  328.                 case "DOMAutoComplete":
  329.                 case "blur":
  330.                     inputElement = event.target;
  331.                     this._pwmgr._fillPassword(inputElement);
  332.                     return;
  333.  
  334.                 default:
  335.                     this._pwmgr.log("Oops! This event unexpected.");
  336.                     return;
  337.             }
  338.         }
  339.     },
  340.  
  341.  
  342.  
  343.  
  344.     /* ---------- Primary Public interfaces ---------- */
  345.  
  346.  
  347.  
  348.  
  349.     /*
  350.      * addLogin
  351.      *
  352.      * Add a new login to login storage.
  353.      */
  354.     addLogin : function (login) {
  355.         // Sanity check the login
  356.         if (login.hostname == null || login.hostname.length == 0)
  357.             throw "Can't add a login with a null or empty hostname.";
  358.  
  359.         // For logins w/o a username, set to "", not null.
  360.         if (login.username == null)
  361.             throw "Can't add a login with a null username.";
  362.  
  363.         if (login.password == null || login.password.length == 0)
  364.             throw "Can't add a login with a null or empty password.";
  365.  
  366.         if (login.formSubmitURL || login.formSubmitURL == "") {
  367.             // We have a form submit URL. Can't have a HTTP realm.
  368.             if (login.httpRealm != null)
  369.                 throw "Can't add a login with both a httpRealm and formSubmitURL.";
  370.         } else if (login.httpRealm) {
  371.             // We have a HTTP realm. Can't have a form submit URL.
  372.             if (login.formSubmitURL != null)
  373.                 throw "Can't add a login with both a httpRealm and formSubmitURL.";
  374.         } else {
  375.             // Need one or the other!
  376.             throw "Can't add a login without a httpRealm or formSubmitURL.";
  377.         }
  378.  
  379.  
  380.         // Look for an existing entry.
  381.         var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
  382.                                      login.httpRealm);
  383.  
  384.         if (logins.some(function(l) login.matches(l, true)))
  385.             throw "This login already exists.";
  386.  
  387.         this.log("Adding login: " + login);
  388.         return this._storage.addLogin(login);
  389.     },
  390.  
  391.  
  392.     /*
  393.      * removeLogin
  394.      *
  395.      * Remove the specified login from the stored logins.
  396.      */
  397.     removeLogin : function (login) {
  398.         this.log("Removing login: " + login);
  399.         return this._storage.removeLogin(login);
  400.     },
  401.  
  402.  
  403.     /*
  404.      * modifyLogin
  405.      *
  406.      * Change the specified login to match the new login.
  407.      */
  408.     modifyLogin : function (oldLogin, newLogin) {
  409.         this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin);
  410.         return this._storage.modifyLogin(oldLogin, newLogin);
  411.     },
  412.  
  413.  
  414.     /*
  415.      * getAllLogins
  416.      *
  417.      * Get a dump of all stored logins. Used by the login manager UI.
  418.      *
  419.      * |count| is only needed for XPCOM.
  420.      *
  421.      * Returns an array of logins. If there are no logins, the array is empty.
  422.      */
  423.     getAllLogins : function (count) {
  424.         this.log("Getting a list of all logins");
  425.         return this._storage.getAllLogins(count);
  426.     },
  427.  
  428.  
  429.     /*
  430.      * removeAllLogins
  431.      *
  432.      * Remove all stored logins.
  433.      */
  434.     removeAllLogins : function () {
  435.         this.log("Removing all logins");
  436.         this._storage.removeAllLogins();
  437.     },
  438.  
  439.     /*
  440.      * getAllDisabledHosts
  441.      *
  442.      * Get a list of all hosts for which logins are disabled.
  443.      *
  444.      * |count| is only needed for XPCOM.
  445.      *
  446.      * Returns an array of disabled logins. If there are no disabled logins,
  447.      * the array is empty.
  448.      */
  449.     getAllDisabledHosts : function (count) {
  450.         this.log("Getting a list of all disabled hosts");
  451.         return this._storage.getAllDisabledHosts(count);
  452.     },
  453.  
  454.  
  455.     /*
  456.      * findLogins
  457.      *
  458.      * Search for the known logins for entries matching the specified criteria.
  459.      */
  460.     findLogins : function (count, hostname, formSubmitURL, httpRealm) {
  461.         this.log("Searching for logins matching host: " + hostname +
  462.             ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
  463.  
  464.         return this._storage.findLogins(count, hostname, formSubmitURL,
  465.                                         httpRealm);
  466.     },
  467.  
  468.  
  469.     /*
  470.      * countLogins
  471.      *
  472.      * Search for the known logins for entries matching the specified criteria,
  473.      * returns only the count.
  474.      */
  475.     countLogins : function (hostname, formSubmitURL, httpRealm) {
  476.         this.log("Counting logins matching host: " + hostname +
  477.             ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
  478.  
  479.         return this._storage.countLogins(hostname, formSubmitURL, httpRealm);
  480.     },
  481.  
  482.  
  483.     /*
  484.      * getLoginSavingEnabled
  485.      *
  486.      * Check to see if user has disabled saving logins for the host.
  487.      */
  488.     getLoginSavingEnabled : function (host) {
  489.         this.log("Checking if logins to " + host + " can be saved.");
  490.         if (!this._remember)
  491.             return false;
  492.  
  493.         return this._storage.getLoginSavingEnabled(host);
  494.     },
  495.  
  496.  
  497.     /*
  498.      * setLoginSavingEnabled
  499.      *
  500.      * Enable or disable storing logins for the specified host.
  501.      */
  502.     setLoginSavingEnabled : function (hostname, enabled) {
  503.         // Nulls won't round-trip with getAllDisabledHosts().
  504.         if (hostname.indexOf("\0") != -1)
  505.             throw "Invalid hostname";
  506.  
  507.         this.log("Saving logins for " + hostname + " enabled? " + enabled);
  508.         return this._storage.setLoginSavingEnabled(hostname, enabled);
  509.     },
  510.  
  511.  
  512.     /*
  513.      * autoCompleteSearch
  514.      *
  515.      * Yuck. This is called directly by satchel:
  516.      * nsFormFillController::StartSearch()
  517.      * [toolkit/components/satchel/src/nsFormFillController.cpp]
  518.      *
  519.      * We really ought to have a simple way for code to register an
  520.      * auto-complete provider, and not have satchel calling pwmgr directly.
  521.      */
  522.     autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) {
  523.         // aPreviousResult & aResult are nsIAutoCompleteResult,
  524.         // aElement is nsIDOMHTMLInputElement
  525.  
  526.         if (!this._remember)
  527.             return false;
  528.  
  529.         this.log("AutoCompleteSearch invoked. Search is: " + aSearchString);
  530.  
  531.         var result = null;
  532.  
  533.         if (aPreviousResult) {
  534.             this.log("Using previous autocomplete result");
  535.             result = aPreviousResult;
  536.  
  537.             // We have a list of results for a shorter search string, so just
  538.             // filter them further based on the new search string.
  539.             // Count backwards, because result.matchCount is decremented
  540.             // when we remove an entry.
  541.             for (var i = result.matchCount - 1; i >= 0; i--) {
  542.                 var match = result.getValueAt(i);
  543.  
  544.                 // Remove results that are too short, or have different prefix.
  545.                 if (aSearchString.length > match.length ||
  546.                     aSearchString.toLowerCase() !=
  547.                         match.substr(0, aSearchString.length).toLowerCase())
  548.                 {
  549.                     this.log("Removing autocomplete entry '" + match + "'");
  550.                     result.removeValueAt(i, false);
  551.                 }
  552.             }
  553.         } else {
  554.             this.log("Creating new autocomplete search result.");
  555.  
  556.             var doc = aElement.ownerDocument;
  557.             var origin = this._getPasswordOrigin(doc.documentURI);
  558.             var actionOrigin = this._getActionOrigin(aElement.form);
  559.  
  560.             var logins = this.findLogins({}, origin, actionOrigin, null);
  561.             var matchingLogins = [];
  562.  
  563.             for (i = 0; i < logins.length; i++) {
  564.                 var username = logins[i].username.toLowerCase();
  565.                 if (aSearchString.length <= username.length &&
  566.                     aSearchString.toLowerCase() ==
  567.                         username.substr(0, aSearchString.length))
  568.                 {
  569.                     matchingLogins.push(logins[i]);
  570.                 }
  571.             }
  572.             this.log(matchingLogins.length + " autocomplete logins avail.");
  573.             result = new UserAutoCompleteResult(aSearchString, matchingLogins);
  574.         }
  575.  
  576.         return result;
  577.     },
  578.  
  579.  
  580.  
  581.  
  582.     /* ------- Internal methods / callbacks for document integration ------- */
  583.  
  584.  
  585.  
  586.  
  587.     /*
  588.      * _getPasswordFields
  589.      *
  590.      * Returns an array of password field elements for the specified form.
  591.      * If no pw fields are found, or if more than 3 are found, then null
  592.      * is returned.
  593.      *
  594.      * skipEmptyFields can be set to ignore password fields with no value.
  595.      */
  596.     _getPasswordFields : function (form, skipEmptyFields) {
  597.         // Locate the password fields in the form.
  598.         var pwFields = [];
  599.         for (var i = 0; i < form.elements.length; i++) {
  600.             if (form.elements[i].type != "password")
  601.                 continue;
  602.  
  603.             if (skipEmptyFields && !form.elements[i].value)
  604.                 continue;
  605.  
  606.             pwFields[pwFields.length] = {
  607.                                             index   : i,
  608.                                             element : form.elements[i]
  609.                                         };
  610.         }
  611.  
  612.         // If too few or too many fields, bail out.
  613.         if (pwFields.length == 0) {
  614.             this.log("(form ignored -- no password fields.)");
  615.             return null;
  616.         } else if (pwFields.length > 3) {
  617.             this.log("(form ignored -- too many password fields. [got " +
  618.                         pwFields.length + "])");
  619.             return null;
  620.         }
  621.  
  622.         return pwFields;
  623.     },
  624.  
  625.  
  626.     /*
  627.      * _getFormFields
  628.      *
  629.      * Returns the username and password fields found in the form.
  630.      * Can handle complex forms by trying to figure out what the
  631.      * relevant fields are.
  632.      *
  633.      * Returns: [usernameField, newPasswordField, oldPasswordField]
  634.      *
  635.      * usernameField may be null.
  636.      * newPasswordField will always be non-null.
  637.      * oldPasswordField may be null. If null, newPasswordField is just
  638.      * "theLoginField". If not null, the form is apparently a
  639.      * change-password field, with oldPasswordField containing the password
  640.      * that is being changed.
  641.      */
  642.     _getFormFields : function (form, isSubmission) {
  643.         var usernameField = null;
  644.  
  645.         // Locate the password field(s) in the form. Up to 3 supported.
  646.         // If there's no password field, there's nothing for us to do.
  647.         var pwFields = this._getPasswordFields(form, isSubmission);
  648.         if (!pwFields)
  649.             return [null, null, null];
  650.  
  651.  
  652.         // Locate the username field in the form by searching backwards
  653.         // from the first passwordfield, assume the first text field is the
  654.         // username. We might not find a username field if the user is
  655.         // already logged in to the site. 
  656.         for (var i = pwFields[0].index - 1; i >= 0; i--) {
  657.             if (form.elements[i].type == "text") {
  658.                 usernameField = form.elements[i];
  659.                 break;
  660.             }
  661.         }
  662.  
  663.         if (!usernameField)
  664.             this.log("(form -- no username field found)");
  665.  
  666.  
  667.         // If we're not submitting a form (it's a page load), there are no
  668.         // password field values for us to use for identifying fields. So,
  669.         // just assume the first password field is the one to be filled in.
  670.         if (!isSubmission || pwFields.length == 1)
  671.             return [usernameField, pwFields[0].element, null];
  672.  
  673.  
  674.         // Try to figure out WTF is in the form based on the password values.
  675.         var oldPasswordField, newPasswordField;
  676.         var pw1 = pwFields[0].element.value;
  677.         var pw2 = pwFields[1].element.value;
  678.         var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
  679.  
  680.         if (pwFields.length == 3) {
  681.             // Look for two identical passwords, that's the new password
  682.  
  683.             if (pw1 == pw2 && pw2 == pw3) {
  684.                 // All 3 passwords the same? Weird! Treat as if 1 pw field.
  685.                 newPasswordField = pwFields[0].element;
  686.                 oldPasswordField = null;
  687.             } else if (pw1 == pw2) {
  688.                 newPasswordField = pwFields[0].element;
  689.                 oldPasswordField = pwFields[2].element;
  690.             } else if (pw2 == pw3) {
  691.                 oldPasswordField = pwFields[0].element;
  692.                 newPasswordField = pwFields[2].element;
  693.             } else  if (pw1 == pw3) {
  694.                 // A bit odd, but could make sense with the right page layout.
  695.                 newPasswordField = pwFields[0].element;
  696.                 oldPasswordField = pwFields[1].element;
  697.             } else {
  698.                 // We can't tell which of the 3 passwords should be saved.
  699.                 this.log("(form ignored -- all 3 pw fields differ)");
  700.                 return [null, null, null];
  701.             }
  702.         } else { // pwFields.length == 2
  703.             if (pw1 == pw2) {
  704.                 // Treat as if 1 pw field
  705.                 newPasswordField = pwFields[0].element;
  706.                 oldPasswordField = null;
  707.             } else {
  708.                 // Just assume that the 2nd password is the new password
  709.                 oldPasswordField = pwFields[0].element;
  710.                 newPasswordField = pwFields[1].element;
  711.             }
  712.         }
  713.  
  714.         return [usernameField, newPasswordField, oldPasswordField];
  715.     },
  716.  
  717.  
  718.     /*
  719.      * _isAutoCompleteDisabled
  720.      *
  721.      * Returns true if the page requests autocomplete be disabled for the
  722.      * specified form input.
  723.      */
  724.     _isAutocompleteDisabled :  function (element) {
  725.         if (element && element.hasAttribute("autocomplete") &&
  726.             element.getAttribute("autocomplete").toLowerCase() == "off")
  727.             return true;
  728.  
  729.         return false;
  730.     },
  731.  
  732.     /*
  733.      * _onFormSubmit
  734.      *
  735.      * Called by the our observer when notified of a form submission.
  736.      * [Note that this happens before any DOM onsubmit handlers are invoked.]
  737.      * Looks for a password change in the submitted form, so we can update
  738.      * our stored password.
  739.      */
  740.     _onFormSubmit : function (form) {
  741.  
  742.         // local helper function
  743.         function getPrompter(aWindow) {
  744.             var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
  745.                               createInstance(Ci.nsILoginManagerPrompter);
  746.             prompterSvc.init(aWindow);
  747.             return prompterSvc;
  748.         }
  749.  
  750.         var doc = form.ownerDocument;
  751.         var win = doc.defaultView;
  752.  
  753.         // If password saving is disabled (globally or for host), bail out now.
  754.         if (!this._remember)
  755.             return;
  756.  
  757.         var hostname      = this._getPasswordOrigin(doc.documentURI);
  758.         var formSubmitURL = this._getActionOrigin(form)
  759.         if (!this.getLoginSavingEnabled(hostname)) {
  760.             this.log("(form submission ignored -- saving is " +
  761.                      "disabled for: " + hostname + ")");
  762.             return;
  763.         }
  764.  
  765.  
  766.         // Get the appropriate fields from the form.
  767.         var [usernameField, newPasswordField, oldPasswordField] =
  768.             this._getFormFields(form, true);
  769.  
  770.         // Need at least 1 valid password field to do anything.
  771.         if (newPasswordField == null)
  772.                 return;
  773.  
  774.         // Check for autocomplete=off attribute. We don't use it to prevent
  775.         // autofilling (for existing logins), but won't save logins when it's
  776.         // present.
  777.         if (this._isAutocompleteDisabled(form) ||
  778.             this._isAutocompleteDisabled(usernameField) ||
  779.             this._isAutocompleteDisabled(newPasswordField) ||
  780.             this._isAutocompleteDisabled(oldPasswordField)) {
  781.                 this.log("(form submission ignored -- autocomplete=off found)");
  782.                 return;
  783.         }
  784.  
  785.  
  786.         var formLogin = new this._nsLoginInfo();
  787.         formLogin.init(hostname, formSubmitURL, null,
  788.                     (usernameField ? usernameField.value : ""),
  789.                     newPasswordField.value,
  790.                     (usernameField ? usernameField.name  : ""),
  791.                     newPasswordField.name);
  792.  
  793.         // If we didn't find a username field, but seem to be changing a
  794.         // password, allow the user to select from a list of applicable
  795.         // logins to update the password for.
  796.         if (!usernameField && oldPasswordField) {
  797.  
  798.             var logins = this.findLogins({}, hostname, formSubmitURL, null);
  799.  
  800.             if (logins.length == 0) {
  801.                 // Could prompt to save this as a new password-only login.
  802.                 // This seems uncommon, and might be wrong, so ignore.
  803.                 this.log("(no logins for this host -- pwchange ignored)");
  804.                 return;
  805.             }
  806.  
  807.             var prompter = getPrompter(win);
  808.  
  809.             if (logins.length == 1) {
  810.                 var oldLogin = logins[0];
  811.                 formLogin.username      = oldLogin.username;
  812.                 formLogin.usernameField = oldLogin.usernameField;
  813.  
  814.                 prompter.promptToChangePassword(oldLogin, formLogin);
  815.             } else {
  816.                 prompter.promptToChangePasswordWithUsernames(
  817.                                     logins, logins.length, formLogin);
  818.             }
  819.  
  820.             return;
  821.         }
  822.  
  823.  
  824.         // Look for an existing login that matches the form login.
  825.         var existingLogin = null;
  826.         var logins = this.findLogins({}, hostname, formSubmitURL, null);
  827.  
  828.         for (var i = 0; i < logins.length; i++) {
  829.             var same, login = logins[i];
  830.  
  831.             // If one login has a username but the other doesn't, ignore
  832.             // the username when comparing and only match if they have the
  833.             // same password. Otherwise, compare the logins and match even
  834.             // if the passwords differ.
  835.             if (!login.username && formLogin.username) {
  836.                 var restoreMe = formLogin.username;
  837.                 formLogin.username = ""; 
  838.                 same = formLogin.matches(login);
  839.                 formLogin.username = restoreMe;
  840.             } else if (!formLogin.username && login.username) {
  841.                 formLogin.username = login.username;
  842.                 same = formLogin.matches(login);
  843.                 formLogin.username = ""; // we know it's always blank.
  844.             } else {
  845.                 same = formLogin.matches(login, true);
  846.             }
  847.  
  848.             if (same) {
  849.                 existingLogin = login;
  850.                 break;
  851.             }
  852.         }
  853.  
  854.         if (existingLogin) {
  855.             this.log("Found an existing login matching this form submission");
  856.  
  857.             /*
  858.              * Change password if needed.
  859.              *
  860.              * If the login has a username, change the password w/o prompting
  861.              * (because we can be fairly sure there's only one password
  862.              * associated with the username). But for logins without a
  863.              * username, ask the user... Some sites use a password-only "login"
  864.              * in different contexts (enter your PIN, answer a security
  865.              * question, etc), and without a username we can't be sure if
  866.              * modifying an existing login is the right thing to do.
  867.              */
  868.             if (existingLogin.password != formLogin.password) {
  869.                 if (formLogin.username) {
  870.                     this.log("...Updating password for existing login.");
  871.                     this.modifyLogin(existingLogin, formLogin);
  872.                 } else {
  873.                     this.log("...passwords differ, prompting to change.");
  874.                     prompter = getPrompter(win);
  875.                     prompter.promptToChangePassword(existingLogin, formLogin);
  876.                 }
  877.             }
  878.  
  879.             return;
  880.         }
  881.  
  882.  
  883.         // Prompt user to save login (via dialog or notification bar)
  884.         prompter = getPrompter(win);
  885.         prompter.promptToSavePassword(formLogin);
  886.     },
  887.  
  888.  
  889.     /*
  890.      * _getPasswordOrigin
  891.      *
  892.      * Get the parts of the URL we want for identification.
  893.      */
  894.     _getPasswordOrigin : function (uriString, allowJS) {
  895.         var realm = "";
  896.         try {
  897.             var uri = this._ioService.newURI(uriString, null, null);
  898.  
  899.             if (allowJS && uri.scheme == "javascript")
  900.                 return "javascript:"
  901.  
  902.             realm = uri.scheme + "://" + uri.host;
  903.  
  904.             // If the URI explicitly specified a port, only include it when
  905.             // it's not the default. (We never want "http://foo.com:80")
  906.             var port = uri.port;
  907.             if (port != -1) {
  908.                 var handler = this._ioService.getProtocolHandler(uri.scheme);
  909.                 if (port != handler.defaultPort)
  910.                     realm += ":" + port;
  911.             }
  912.  
  913.         } catch (e) {
  914.             // bug 159484 - disallow url types that don't support a hostPort.
  915.             // (although we handle "javascript:..." as a special case above.)
  916.             this.log("Couldn't parse origin for " + uriString);
  917.             realm = null;
  918.         }
  919.  
  920.         return realm;
  921.     },
  922.  
  923.     _getActionOrigin : function (form) {
  924.         var uriString = form.action;
  925.  
  926.         // A blank or mission action submits to where it came from.
  927.         if (uriString == "")
  928.             uriString = form.baseURI; // ala bug 297761
  929.  
  930.         return this._getPasswordOrigin(uriString, true);
  931.     },
  932.  
  933.  
  934.     /*
  935.      * _fillDocument
  936.      *
  937.      * Called when a page has loaded. For each form in the document,
  938.      * we check to see if it can be filled with a stored login.
  939.      */
  940.     _fillDocument : function (doc) {
  941.         var forms = doc.forms;
  942.         if (!forms || forms.length == 0)
  943.             return;
  944.  
  945.         var formOrigin = this._getPasswordOrigin(doc.documentURI);
  946.  
  947.         // If there are no logins for this site, bail out now.
  948.         if (!this.countLogins(formOrigin, "", null))
  949.             return;
  950.  
  951.         this.log("fillDocument processing " + forms.length +
  952.                  " forms on " + doc.documentURI);
  953.  
  954.         var autofillForm = this._prefBranch.getBoolPref("autofillForms");
  955.         var previousActionOrigin = null;
  956.  
  957.         for (var i = 0; i < forms.length; i++) {
  958.             var form = forms[i];
  959.  
  960.             // Heuristically determine what the user/pass fields are
  961.             // We do this before checking to see if logins are stored,
  962.             // so that the user isn't prompted for a master password
  963.             // without need.
  964.             var [usernameField, passwordField, ignored] =
  965.                 this._getFormFields(form, false);
  966.  
  967.             // Need a valid password field to do anything.
  968.             if (passwordField == null)
  969.                 continue;
  970.  
  971.  
  972.             // Only the actionOrigin might be changing, so if it's the same
  973.             // as the last form on the page we can reuse the same logins.
  974.             var actionOrigin = this._getActionOrigin(form);
  975.             if (actionOrigin != previousActionOrigin) {
  976.                 var foundLogins =
  977.                     this.findLogins({}, formOrigin, actionOrigin, null);
  978.  
  979.                 this.log("form[" + i + "]: found " + foundLogins.length +
  980.                         " matching logins.");
  981.  
  982.                 previousActionOrigin = actionOrigin;
  983.             } else {
  984.                 this.log("form[" + i + "]: reusing logins from last form.");
  985.             }
  986.  
  987.  
  988.             // Discard logins which have username/password values that don't
  989.             // fit into the fields (as specified by the maxlength attribute).
  990.             // The user couldn't enter these values anyway, and it helps
  991.             // with sites that have an extra PIN to be entered (bug 391514)
  992.             var maxUsernameLen = Number.MAX_VALUE;
  993.             var maxPasswordLen = Number.MAX_VALUE;
  994.  
  995.             // If attribute wasn't set, default is -1.
  996.             if (usernameField && usernameField.maxLength >= 0)
  997.                 maxUsernameLen = usernameField.maxLength;
  998.             if (passwordField.maxLength >= 0)
  999.                 maxPasswordLen = passwordField.maxLength;
  1000.  
  1001.             logins = foundLogins.filter(function (l) {
  1002.                     var fit = (l.username.length <= maxUsernameLen &&
  1003.                                l.password.length <= maxPasswordLen);
  1004.                     if (!fit)
  1005.                         this.log("Ignored " + l.username + " login: won't fit");
  1006.  
  1007.                     return fit;
  1008.                 }, this);
  1009.  
  1010.  
  1011.             // Nothing to do if we have no matching logins available.
  1012.             if (logins.length == 0)
  1013.                 continue;
  1014.  
  1015.  
  1016.             // Attach autocomplete stuff to the username field, if we have
  1017.             // one. This is normally used to select from multiple accounts,
  1018.             // but even with one account we should refill if the user edits.
  1019.             if (usernameField)
  1020.                 this._attachToInput(usernameField);
  1021.  
  1022.             // If the form has an autocomplete=off attribute in play, don't
  1023.             // fill in the login automatically. We check this after attaching
  1024.             // the autocomplete stuff to the username field, so the user can
  1025.             // still manually select a login to be filled in.
  1026.             var isFormDisabled = false;
  1027.             if (this._isAutocompleteDisabled(form) ||
  1028.                 this._isAutocompleteDisabled(usernameField) ||
  1029.                 this._isAutocompleteDisabled(passwordField)) {
  1030.  
  1031.                 isFormDisabled = true;
  1032.                 this.log("form[" + i + "]: not filled, has autocomplete=off");
  1033.             }
  1034.  
  1035.             if (autofillForm && !isFormDisabled) {
  1036.  
  1037.                 if (usernameField && usernameField.value) {
  1038.                     // If username was specified in the form, only fill in the
  1039.                     // password if we find a matching login.
  1040.  
  1041.                     var username = usernameField.value;
  1042.  
  1043.                     var matchingLogin;
  1044.                     var found = logins.some(function(l) {
  1045.                                                 matchingLogin = l;
  1046.                                                 return (l.username == username);
  1047.                                             });
  1048.                     if (found)
  1049.                         passwordField.value = matchingLogin.password;
  1050.                     else
  1051.                         this.log("Password not filled. None of the stored " +
  1052.                                  "logins match the username already present.");
  1053.  
  1054.                 } else if (usernameField && logins.length == 2) {
  1055.                     // Special case, for sites which have a normal user+pass
  1056.                     // login *and* a password-only login (eg, a PIN)...
  1057.                     // When we have a username field and 1 of 2 available
  1058.                     // logins is password-only, go ahead and prefill the
  1059.                     // one with a username.
  1060.                     if (!logins[0].username && logins[1].username) {
  1061.                         usernameField.value = logins[1].username;
  1062.                         passwordField.value = logins[1].password;
  1063.                     } else if (!logins[1].username && logins[0].username) {
  1064.                         usernameField.value = logins[0].username;
  1065.                         passwordField.value = logins[0].password;
  1066.                     }
  1067.                 } else if (logins.length == 1) {
  1068.                     if (usernameField)
  1069.                         usernameField.value = logins[0].username;
  1070.                     passwordField.value = logins[0].password;
  1071.                 } else {
  1072.                     this.log("Multiple logins for form, so not filling any.");
  1073.                 }
  1074.             }
  1075.         } // foreach form
  1076.     },
  1077.  
  1078.  
  1079.     /*
  1080.      * _attachToInput
  1081.      *
  1082.      * Hooks up autocomplete support to a username field, to allow
  1083.      * a user editing the field to select an existing login and have
  1084.      * the password field filled in.
  1085.      */
  1086.     _attachToInput : function (element) {
  1087.         this.log("attaching autocomplete stuff");
  1088.         element.addEventListener("blur",
  1089.                                 this._domEventListener, false);
  1090.         element.addEventListener("DOMAutoComplete",
  1091.                                 this._domEventListener, false);
  1092.         this._formFillService.markAsLoginManagerField(element);
  1093.     },
  1094.  
  1095.  
  1096.     /*
  1097.      * _fillPassword
  1098.      *
  1099.      * The user has autocompleted a username field, so fill in the password.
  1100.      */
  1101.     _fillPassword : function (usernameField) {
  1102.         this.log("fillPassword autocomplete username: " + usernameField.value);
  1103.  
  1104.         var form = usernameField.form;
  1105.         var doc = form.ownerDocument;
  1106.  
  1107.         var hostname = this._getPasswordOrigin(doc.documentURI);
  1108.         var formSubmitURL = this._getActionOrigin(form)
  1109.  
  1110.         // Find the password field. We should always have at least one,
  1111.         // or else something has gone rather wrong.
  1112.         var pwFields = this._getPasswordFields(form, false);
  1113.         if (!pwFields) {
  1114.             const err = "No password field for autocomplete password fill.";
  1115.  
  1116.             // We want to know about this even if debugging is disabled.
  1117.             if (!this._debug)
  1118.                 dump(err);
  1119.             else
  1120.                 this.log(err);
  1121.  
  1122.             return;
  1123.         }
  1124.  
  1125.         // If there are multiple passwords fields, we can't really figure
  1126.         // out what each field is for, so just fill out the last field.
  1127.         var passwordField = pwFields[0].element;
  1128.  
  1129.         // Temporary LoginInfo with the info we know.
  1130.         var currentLogin = new this._nsLoginInfo();
  1131.         currentLogin.init(hostname, formSubmitURL, null,
  1132.                           usernameField.value, null,
  1133.                           usernameField.name, passwordField.name);
  1134.  
  1135.         // Look for a existing login and use its password.
  1136.         var match = null;
  1137.         var logins = this.findLogins({}, hostname, formSubmitURL, null);
  1138.  
  1139.         if (!logins.some(function(l) {
  1140.                                 match = l;
  1141.                                 return currentLogin.matches(l, true);
  1142.                         }))
  1143.         {
  1144.             this.log("Can't find a login for this autocomplete result.");
  1145.             return;
  1146.         }
  1147.  
  1148.         this.log("Found a matching login, filling in password.");
  1149.         passwordField.value = match.password;
  1150.     }
  1151. }; // end of LoginManager implementation
  1152.  
  1153.  
  1154.  
  1155.  
  1156. // nsIAutoCompleteResult implementation
  1157. function UserAutoCompleteResult (aSearchString, matchingLogins) {
  1158.     function loginSort(a,b) {
  1159.         var userA = a.username.toLowerCase();
  1160.         var userB = b.username.toLowerCase();
  1161.  
  1162.         if (userA < userB)
  1163.             return -1;
  1164.  
  1165.         if (userB > userA)
  1166.             return  1;
  1167.  
  1168.         return 0;
  1169.     };
  1170.  
  1171.     this.searchString = aSearchString;
  1172.     this.logins = matchingLogins.sort(loginSort);
  1173.     this.matchCount = matchingLogins.length;
  1174.  
  1175.     if (this.matchCount > 0) {
  1176.         this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
  1177.         this.defaultIndex = 0;
  1178.     }
  1179. }
  1180.  
  1181. UserAutoCompleteResult.prototype = {
  1182.     QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
  1183.                                             Ci.nsISupportsWeakReference]),
  1184.  
  1185.     // private
  1186.     logins : null,
  1187.  
  1188.     // Interfaces from idl...
  1189.     searchString : null,
  1190.     searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
  1191.     defaultIndex : -1,
  1192.     errorDescription : "",
  1193.     matchCount : 0,
  1194.  
  1195.     getValueAt : function (index) {
  1196.         if (index < 0 || index >= this.logins.length)
  1197.             throw "Index out of range.";
  1198.  
  1199.         return this.logins[index].username;
  1200.     },
  1201.  
  1202.     getCommentAt : function (index) {
  1203.         return "";
  1204.     },
  1205.  
  1206.     getStyleAt : function (index) {
  1207.         return "";
  1208.     },
  1209.  
  1210.     getImageAt : function (index) {
  1211.         return "";
  1212.     },
  1213.  
  1214.     removeValueAt : function (index, removeFromDB) {
  1215.         if (index < 0 || index >= this.logins.length)
  1216.             throw "Index out of range.";
  1217.  
  1218.         var [removedLogin] = this.logins.splice(index, 1);
  1219.  
  1220.         this.matchCount--;
  1221.         if (this.defaultIndex > this.logins.length)
  1222.             this.defaultIndex--;
  1223.  
  1224.         if (removeFromDB) {
  1225.             var pwmgr = Cc["@mozilla.org/login-manager;1"].
  1226.                         getService(Ci.nsILoginManager);
  1227.             pwmgr.removeLogin(removedLogin);
  1228.         }
  1229.     },
  1230. };
  1231.  
  1232. var component = [LoginManager];
  1233. function NSGetModule (compMgr, fileSpec) {
  1234.     return XPCOMUtils.generateModule(component);
  1235. }
  1236.